/* Copyright 2011 Brian Cairns
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.wsnetwork.reforger;
import indzcraft.loader.Equipment;
import indzcraft.loader.ItemEquiped;

import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import net.wsnetwork.reforger.rogue.CombatSpecModel;
import net.wsnetwork.reforger.rogue.MutiltateSpecModel;

public class Reforger {
    private static final int FROM_INDEX = 0;
    private static final int TO_INDEX = 1;
    private static final int VALUE_INDEX = 2;

    private static final int NUM_INTERNET_THREADS = 4;

    private Float bestEp = 0.0f;
    private int[] bestOptions;
    private int[][][] reforgingMatrix;
    private float[] maxEpDelta;
    private StatVector noReforgingStats;

    private List<Item> baseItems;

    private PrintStream outputStream = System.out;
    private URLRetriever urlRetriever;
    private SpecModel model;
    
    private Set<Item> reforgedItems;
    private Set<Item> notReforgedItems;
    
    private List<ReforgingOption> results = null;
    
    public Reforger(PrintStream outputStream, URLRetriever urlRetriever) {
        this.outputStream = outputStream;
        this.urlRetriever = urlRetriever;
    }
    
    public Reforger(){
    	
    }

    private void printStats(String msg, StatVector stats) {
        float ep = model.calculateEp(stats.getData());
        outputStream.println(String.format(msg, ep));
        for (Stat stat : Stat.possibleStats) {
            outputStream.println("  " + stat.getName() + ": " + stats.getStat(stat));
        }
    }

    private void printDelta(String msg, StatVector delta) {
        outputStream.println(msg);
        for (Stat stat : Stat.possibleStats) {
            int value = delta.getStat(stat);
            outputStream.println("  " + stat.getName() + ": " + (value > 0 ? "+" : "") + value);
        }
    }

    private static String formatBigNumber(double number) {
        if (number > 1000000000) {
            return String.format("%.2f", number / 1000000000.0) + "B";
        } else if (number > 10000000) {
            return String.format("%.0f", number / 1000000.0) + "M";
        } else if (number > 1000000) {
            return String.format("%.2f", number / 1000000.0) + "M";
        } else if (number > 1000) {
            return String.format("%.1f", number / 1000.0) + "K";
        } else {
            return String.format("%.2f", number);
        }
    }

    public class ItemRetrieverRunnable implements Runnable {
        private final int itemId;
        private final PrintStream outputStream;

        public ItemRetrieverRunnable(int itemId, PrintStream outputStream) {
            this.itemId = itemId;
            this.outputStream = outputStream;
        }

        @Override public void run() {
            Item item = new Item(itemId, urlRetriever);
            outputStream.println("  " + item);
            baseItems.add(item);
        }
    }

    public void runReforging2(HashMap<String,Double> ep,Equipment eq) throws InterruptedException, IOException {
        // Print version info
        outputStream.println("Reforger version " + Version.APP_VERSION + ".");

        // Get character info
        outputStream.println("Loading character info:");
        // TODO infos uebermitteln welche Skillung das ist, evtl. ja eh überflüssig
        model = new CombatSpecModel(ep);

        // Get item stats
        outputStream.println("Loading items: ");
        baseItems = Collections.synchronizedList(new ArrayList<Item>());
        for(String name : eq.equipment.keySet()){
			ItemEquiped iq = eq.get(name);
			StatVector sv = new StatVector(iq.getHit(), iq.getMastery(), iq.getHaste(), iq.getExp(), iq.getCrit());
			Item it = new Item(name,iq.getName(),sv);
			
			baseItems.add(it);
		}

        // Calculate stats
        StatVector baseStats = null;
        StatVector reforgings = new StatVector(eq.getReforgings());
        StatVector currentStats = new StatVector(eq.getStats());
        for (Item item : baseItems) {
            if (item.hasRandomEnchant()) {
                throw new RuntimeException("Random enchant item found. "
                        + "These items are not yet supported.");
            }
            if (baseStats == null) {
                baseStats = item.getStats();
            } else {
                baseStats = baseStats.add(item.getStats());
            }
        }
        noReforgingStats = currentStats.subtract(reforgings);
        StatVector bonuses = noReforgingStats.subtract(baseStats);

        // Print stats
        printStats("Base stats:", baseStats);
        printDelta("Gem/enchant bonuses:", bonuses);
        printStats("Stats with no reforgings (EP = %.1f):", noReforgingStats);
        printDelta("Current reforgings:", reforgings);
        printStats("Stats with current reforgings (EP = %.1f):", currentStats);

        // Determine best current EP as a baseline
        float currentStatEp = model.calculateEp(noReforgingStats.getData());
        float noReforgingEp = model.calculateEp(currentStats.getData());
        bestEp = Math.max(currentStatEp, noReforgingEp) - 1;

        // Create option arrays
        Map<Item, Float> itemMaxEpDelta = new HashMap<Item, Float>();
        List<List<ReforgingOption>> reforgingOptions = new ArrayList<List<ReforgingOption>>();
        List<ReforgingOption> allPossibleReforgings = new ArrayList<ReforgingOption>();
        int maxNumberReforgingOptions = 0;
        for (Item item : baseItems) {
            float maxItemDelta = 0;
            List<ReforgingOption> itemReforgingOptions = new ArrayList<ReforgingOption>();
            Collection<StatVector> itemPossibleReforgings =
                item.getPossibleReforgings(model.getPossibleReforgings());
            for (StatVector delta : itemPossibleReforgings) {
                ReforgingOption option = new ReforgingOption(item, delta);
                allPossibleReforgings.add(option);
                itemReforgingOptions.add(option);
                // Calculate the EP delta upper bound for reforging this item
                for (Stat from : Stat.possibleStats) {
                    int statValue = delta.getStat(from);
                    if (statValue < 0) {
                        float epDelta = model.calculateEpMaxDelta(from, Math.abs(statValue));
                        if (epDelta > maxItemDelta) {
                            maxItemDelta = epDelta;
                        }
                    }
                }
            }
            if (itemReforgingOptions.size() > 0) {
                if (itemReforgingOptions.size() > maxNumberReforgingOptions) {
                    maxNumberReforgingOptions = itemReforgingOptions.size();
                }
                itemMaxEpDelta.put(item, maxItemDelta);
                reforgingOptions.add(itemReforgingOptions);
            }
        }

        // Print reforging options
        outputStream.println("Considering " + allPossibleReforgings.size() + " possible reforgings:");
        for (ReforgingOption option : allPossibleReforgings) {
            outputStream.println("  " + option);
        }

        // Calculate max EP delta for sublists
        maxEpDelta = new float[reforgingOptions.size()];
        for (int i = 0; i < reforgingOptions.size(); ++i) {
            float sublistEpDelta = 0;
            for (int j = i + 1; j < reforgingOptions.size(); ++j) {
                Item item = reforgingOptions.get(j).get(0).getItem();
                sublistEpDelta += itemMaxEpDelta.get(item);
            }
            maxEpDelta[i] = sublistEpDelta;
        }

        // Create reforging matrix and result array
        reforgingMatrix = new int[reforgingOptions.size()][maxNumberReforgingOptions][3];
        bestOptions = new int[reforgingOptions.size()];
        for (int i = 0; i < reforgingOptions.size(); ++i) {
            List<ReforgingOption> itemOptions = reforgingOptions.get(i);
            for (int j = 0; j < maxNumberReforgingOptions; ++j) {
                if (j < itemOptions.size()) {
                    ReforgingOption option = itemOptions.get(j);
                    reforgingMatrix[i][j][FROM_INDEX] = option.getFromStatIndex();
                    reforgingMatrix[i][j][TO_INDEX] = option.getToStatIndex();
                    reforgingMatrix[i][j][VALUE_INDEX] = option.getDeltaValue();
                } else {
                    reforgingMatrix[i][j][FROM_INDEX] = -1;
                    reforgingMatrix[i][j][TO_INDEX] = -1;
                    reforgingMatrix[i][j][VALUE_INDEX] = 0;
                }
            }
        }

        long startTime = System.currentTimeMillis();

        if (reforgingOptions.size() < 2) {
            throw new UnsupportedOperationException(
            "You must have at least two reforgable items to use this tool.");
        }

        outputStream.println("Calculating...");
        getBestOptions(noReforgingStats.getData(), 0);

        // Decode result array
        results = new ArrayList<ReforgingOption>();
        for (int i = 0; i < bestOptions.length; ++i) {
            if (bestOptions[i] != -1) {
                results.add(reforgingOptions.get(i).get(bestOptions[i]));
            }
        }

        outputStream.println("Recommended reforgings: ");
        reforgedItems = new HashSet<Item>();
        for (ReforgingOption option : results) {
            outputStream.println("  " + option);
            reforgedItems.add(option.getItem());
        }

        notReforgedItems = new HashSet<Item>();
        outputStream.println("Should not be reforged: ");
        for (ReforgingOption option : allPossibleReforgings) {
            Item item = option.getItem();
            if (!reforgedItems.contains(item)) {
                notReforgedItems.add(item);
            }
        }
        for (Item item : notReforgedItems) {
            outputStream.println("  " + item.getName());
        }

        printStats("Stats with no reforgings (EP = %.1f):", noReforgingStats);
        printStats("Stats with current reforgings (EP = %.1f):", currentStats);

        // Calculate stats after recommended reforgings
        StatVector recommendStats = noReforgingStats;
        for (ReforgingOption option : results) {
            recommendStats = recommendStats.add(option.getDelta());
        }

        double currentEp = model.calculateEp(currentStats.getData());
        double recommendedEp = model.calculateEp(recommendStats.getData());
        double improvement = Math.round((recommendedEp - currentEp) * 10) / 10.0;
        printStats("Stats with recommended reforgings (EP = %.1f):", recommendStats);
        outputStream.println("Improvement over current reforgings: " + improvement);


        long endTime = System.currentTimeMillis();
        double time = (endTime - startTime) / 1000.0;
        outputStream.println("Calculation time: " + formatBigNumber(time) + " seconds ");
    }
    
    public void runReforging(String characterUrl) throws InterruptedException, IOException {
        // Update character URL
        characterUrl = characterUrl.replaceAll("/simple$", "/advanced");
        characterUrl = characterUrl.replaceAll("wow/\\w\\w/character", "wow/en/character");

        if (!characterUrl.endsWith("/advanced")) {
            throw new RuntimeException("Character URL must end with \"/advanced\"");
        }

        // Print version info
        outputStream.println("Reforger version " + Version.APP_VERSION + ".");

        // Get character info
        outputStream.println("Loading character info:");
        Character character = new Character(characterUrl, urlRetriever);
        Collection<Integer> itemIds = character.getItemIds();
        if (character.isCombat()) {
            outputStream.println("  Using combat reforgings.");
            model = new CombatSpecModel();
        } else {
            outputStream.println("  Using mutilate reforgings.");
            model = new MutiltateSpecModel();
        }

        // Check if we got items
        if (itemIds.size() == 0) {
            throw new RuntimeException("No items were found on character URL: " + characterUrl);
        }

        // Get item stats
        outputStream.println("Loading items: ");
        baseItems = Collections.synchronizedList(new ArrayList<Item>());
        ExecutorService netExec = Executors.newFixedThreadPool(NUM_INTERNET_THREADS);
        for (int itemId : itemIds) {
            netExec.submit(new ItemRetrieverRunnable(itemId, outputStream));
        }
        netExec.shutdown();
        if (!netExec.awaitTermination(60, TimeUnit.SECONDS)) {
            throw new RuntimeException("Timout while loading item information.");
        }

        // Calculate stats
        StatVector baseStats = null;
        StatVector reforgings = character.getReforgings();
        StatVector currentStats = character.getStats();
        for (Item item : baseItems) {
            if (item.hasRandomEnchant()) {
                throw new RuntimeException("Random enchant item found. "
                        + "These items are not yet supported.");
            }
            if (baseStats == null) {
                baseStats = item.getStats();
            } else {
                baseStats = baseStats.add(item.getStats());
            }
        }
        noReforgingStats = currentStats.subtract(reforgings);
        StatVector bonuses = noReforgingStats.subtract(baseStats);

        // Print stats
        printStats("Base stats:", baseStats);
        printDelta("Gem/enchant bonuses:", bonuses);
        printStats("Stats with no reforgings (EP = %.1f):", noReforgingStats);
        printDelta("Current reforgings:", reforgings);
        printStats("Stats with current reforgings (EP = %.1f):", currentStats);

        // Determine best current EP as a baseline
        float currentStatEp = model.calculateEp(noReforgingStats.getData());
        float noReforgingEp = model.calculateEp(currentStats.getData());
        bestEp = Math.max(currentStatEp, noReforgingEp) - 1;

        // Create option arrays
        Map<Item, Float> itemMaxEpDelta = new HashMap<Item, Float>();
        List<List<ReforgingOption>> reforgingOptions = new ArrayList<List<ReforgingOption>>();
        List<ReforgingOption> allPossibleReforgings = new ArrayList<ReforgingOption>();
        int maxNumberReforgingOptions = 0;
        for (Item item : baseItems) {
            float maxItemDelta = 0;
            List<ReforgingOption> itemReforgingOptions = new ArrayList<ReforgingOption>();
            Collection<StatVector> itemPossibleReforgings =
                item.getPossibleReforgings(model.getPossibleReforgings());
            for (StatVector delta : itemPossibleReforgings) {
                ReforgingOption option = new ReforgingOption(item, delta);
                allPossibleReforgings.add(option);
                itemReforgingOptions.add(option);
                // Calculate the EP delta upper bound for reforging this item
                for (Stat from : Stat.possibleStats) {
                    int statValue = delta.getStat(from);
                    if (statValue < 0) {
                        float epDelta = model.calculateEpMaxDelta(from, Math.abs(statValue));
                        if (epDelta > maxItemDelta) {
                            maxItemDelta = epDelta;
                        }
                    }
                }
            }
            if (itemReforgingOptions.size() > 0) {
                if (itemReforgingOptions.size() > maxNumberReforgingOptions) {
                    maxNumberReforgingOptions = itemReforgingOptions.size();
                }
                itemMaxEpDelta.put(item, maxItemDelta);
                reforgingOptions.add(itemReforgingOptions);
            }
        }

        // Print reforging options
        outputStream.println("Considering " + allPossibleReforgings.size() + " possible reforgings:");
        for (ReforgingOption option : allPossibleReforgings) {
            outputStream.println("  " + option);
        }

        // Calculate max EP delta for sublists
        maxEpDelta = new float[reforgingOptions.size()];
        for (int i = 0; i < reforgingOptions.size(); ++i) {
            float sublistEpDelta = 0;
            for (int j = i + 1; j < reforgingOptions.size(); ++j) {
                Item item = reforgingOptions.get(j).get(0).getItem();
                sublistEpDelta += itemMaxEpDelta.get(item);
            }
            maxEpDelta[i] = sublistEpDelta;
        }

        // Create reforging matrix and result array
        reforgingMatrix = new int[reforgingOptions.size()][maxNumberReforgingOptions][3];
        bestOptions = new int[reforgingOptions.size()];
        for (int i = 0; i < reforgingOptions.size(); ++i) {
            List<ReforgingOption> itemOptions = reforgingOptions.get(i);
            for (int j = 0; j < maxNumberReforgingOptions; ++j) {
                if (j < itemOptions.size()) {
                    ReforgingOption option = itemOptions.get(j);
                    reforgingMatrix[i][j][FROM_INDEX] = option.getFromStatIndex();
                    reforgingMatrix[i][j][TO_INDEX] = option.getToStatIndex();
                    reforgingMatrix[i][j][VALUE_INDEX] = option.getDeltaValue();
                } else {
                    reforgingMatrix[i][j][FROM_INDEX] = -1;
                    reforgingMatrix[i][j][TO_INDEX] = -1;
                    reforgingMatrix[i][j][VALUE_INDEX] = 0;
                }
            }
        }

        long startTime = System.currentTimeMillis();

        if (reforgingOptions.size() < 2) {
            throw new UnsupportedOperationException(
            "You must have at least two reforgable items to use this tool.");
        }

        outputStream.println("Calculating...");
        getBestOptions(noReforgingStats.getData(), 0);

        // Decode result array
        results = new ArrayList<ReforgingOption>();
        for (int i = 0; i < bestOptions.length; ++i) {
            if (bestOptions[i] != -1) {
                results.add(reforgingOptions.get(i).get(bestOptions[i]));
            }
        }

        outputStream.println("Recommended reforgings: ");
        Set<Item> reforgedItems = new HashSet<Item>();
        for (ReforgingOption option : results) {
            outputStream.println("  " + option);
            reforgedItems.add(option.getItem());
        }

        Set<Item> notReforgedItems = new HashSet<Item>();
        outputStream.println("Should not be reforged: ");
        for (ReforgingOption option : allPossibleReforgings) {
            Item item = option.getItem();
            if (!reforgedItems.contains(item)) {
                notReforgedItems.add(item);
            }
        }
        for (Item item : notReforgedItems) {
            outputStream.println("  " + item.getName());
        }

        printStats("Stats with no reforgings (EP = %.1f):", noReforgingStats);
        printStats("Stats with current reforgings (EP = %.1f):", currentStats);

        // Calculate stats after recommended reforgings
        StatVector recommendStats = noReforgingStats;
        for (ReforgingOption option : results) {
            recommendStats = recommendStats.add(option.getDelta());
        }

        double currentEp = model.calculateEp(currentStats.getData());
        double recommendedEp = model.calculateEp(recommendStats.getData());
        double improvement = Math.round((recommendedEp - currentEp) * 10) / 10.0;
        printStats("Stats with recommended reforgings (EP = %.1f):", recommendStats);
        outputStream.println("Improvement over current reforgings: " + improvement);


        long endTime = System.currentTimeMillis();
        double time = (endTime - startTime) / 1000.0;
        outputStream.println("Calculation time: " + formatBigNumber(time) + " seconds ");
    }

    private boolean getBestOptions(int[] stats, int depth) {
        int[][] currentOptions = reforgingMatrix[depth];
        boolean improved = false;

        // Base case
        if (depth + 1 == maxEpDelta.length) {
            for (int i = 0; i < currentOptions.length; ++i) {
                int fromIndex = currentOptions[i][FROM_INDEX];
                if (fromIndex == -1) { break; }
                int toIndex = currentOptions[i][TO_INDEX];
                int deltaValue = currentOptions[i][VALUE_INDEX];
                stats[fromIndex] -= deltaValue;
                stats[toIndex] += deltaValue;
                float afterReforgingEp = model.calculateEp(stats);
                if (afterReforgingEp > bestEp) {
                    bestEp = afterReforgingEp;
                    bestOptions[depth] = i;
                    improved = true;
                }
                stats[fromIndex] += deltaValue;
                stats[toIndex] -= deltaValue;
            }
            return improved;
        }

        double currentMaxEpDelta = maxEpDelta[depth];
        for (int i = 0; i < currentOptions.length; ++i) {
            int fromIndex = currentOptions[i][FROM_INDEX];
            if (fromIndex == -1) { break; }
            int toIndex = currentOptions[i][TO_INDEX];
            int deltaValue = currentOptions[i][VALUE_INDEX];
            stats[fromIndex] -= deltaValue;
            stats[toIndex] += deltaValue;
            float afterReforgingEp = model.calculateEp(stats);
            if (afterReforgingEp + currentMaxEpDelta > bestEp) {
                if (getBestOptions(stats, depth + 1)) {
                    bestOptions[depth] = i;
                    improved = true;
                }
            }
            stats[fromIndex] += deltaValue;
            stats[toIndex] -= deltaValue;
        }

        float currentEp = model.calculateEp(stats);
        if (currentEp + currentMaxEpDelta > bestEp) {
            if (getBestOptions(stats, depth + 1)) {
                bestOptions[depth] = -1;
                improved = true;
            }
        }

        return improved;
    }
       
    public List<ReforgingOption> getResults(){
    	return results;
    }
    
    public Set<Item> getNotReforgedItems(){
    	return notReforgedItems;
    }
}
